package com.jiawa.train.business.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.jiawa.train.business.config.TokenRateProperties;
import com.jiawa.train.business.domain.SkToken;
import com.jiawa.train.business.domain.SkTokenExample;
import com.jiawa.train.business.enums.CacheResultEnum;
import com.jiawa.train.business.enums.RedisKeyPreEnum;
import com.jiawa.train.business.enums.SeatTypeEnum;
import com.jiawa.train.business.enums.SkTokenEnum;
import com.jiawa.train.business.mapper.SkTokenMapper;
import com.jiawa.train.business.mapper.cust.SkTokenMapperCust;
import com.jiawa.train.business.req.ConfirmOrderTicketReq;
import com.jiawa.train.business.req.SkTokenQueryReq;
import com.jiawa.train.business.req.SkTokenSaveReq;
import com.jiawa.train.common.exception.BusinessException;
import com.jiawa.train.common.exception.BusinessExceptionEnum;
import com.jiawa.train.common.resp.PageResult;
import com.jiawa.train.common.util.SnowUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class SkTokenService {

    @Resource
    private SkTokenMapper skTokenMapper;

    @Resource
    private DailyTrainSeatService dailyTrainSeatService;

    @Resource
    private TokenRateProperties tokenRateProperties;

    @Resource
    private SkTokenMapperCust skTokenMapperCust;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private RedisTemplate<String,Object> redisTemplate;

    @Resource
    private RedisTemplate<String,Long> longRedisTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final String LUA_SCRIPT = "local tokenKey = KEYS[1] " +
            "local counterKey = KEYS[2] " +
            "if redis.call('exists', tokenKey) == 0 then " +
            "return 'NEED_DB_CHECK_ONE' " +
            "end " +
            "local currentToken = tonumber(redis.call('get', tokenKey)) " +
            "local newTokenCount = currentToken - 1 " +
            "if newTokenCount < 0 then " +
            "return 'NOT_ENOUGH' " +
            "end " +
            "redis.call('set', tokenKey, newTokenCount) " +
            "redis.call('expire', tokenKey, 400) " +
            "local currentCounter = redis.call('incr', counterKey) " +
            "redis.call('expire', counterKey, 200) " +
            "return newTokenCount .. ':' .. currentCounter";

    //新增或者更新用户信息，通过判断是否有id决定进行新增操作还是更新操作
    public void save(SkTokenSaveReq req) {
        //将传递的参数赋给SkToken对象,在req中没有id,createTime,updateTime的数据，需要自己进行赋值
        //这里的id使用雪花算法，然后创建时间使用当前时间
        DateTime now = DateTime.now();
        SkToken skToken = BeanUtil.copyProperties(req, SkToken.class);
        //如果id为空，则进行新增操作
        if(ObjectUtil.isNull(skToken.getId())){
            skToken.setId(SnowUtil.getSnowflakeNextId());
            skToken.setCreateTime(now);
            skToken.setUpdateTime(now);
            skTokenMapper.insert(skToken);
        }
        //否则进行更新操作
        else{
            skToken.setUpdateTime(now);
            skTokenMapper.updateByPrimaryKeySelective( skToken);
        }
    }

    //如果用户id存在则进行根据id进行查询乘客的信息，如果是用户id不存在则进行查询所有,全部都按分页进行处理
    public PageResult querySkTokenList(SkTokenQueryReq  req) {
        //创建skTokenExample对象
        SkTokenExample skTokenExample = new SkTokenExample();
        skTokenExample.setOrderByClause("id desc");
        // 先设置分页的参数
        PageHelper.startPage(req.getPage(),req.getPageSize());
        //进行查询数据库
        List<SkToken> skTokenList = skTokenMapper.selectByExample(skTokenExample);
        //返回查询结果,转为分页的形式,
        PageInfo<SkToken> pageInfo = new PageInfo<>(skTokenList);
        //获取数据进行封装
        return new PageResult(pageInfo.getTotal(), pageInfo.getList());
    }

    public void delete(Long id) {
        skTokenMapper.deleteByPrimaryKey(id);
    }


    //先根据日期进行删除每次车次的令牌信息，然后在根据日期和车次信息查询一等座座位个数和二等座座位个数，然后进行令牌信息进行保存
    public void genDailySkToken(Date date, String trainCode) {
        DateTime now = DateTime.now();
        //根据日期和车次进行删除令牌信息
        SkTokenExample skTokenExample = new SkTokenExample();
        SkTokenExample.Criteria criteria = skTokenExample.createCriteria();
        criteria.andDateEqualTo(date);
        criteria.andTrainCodeEqualTo(trainCode);
        skTokenMapper.deleteByExample(skTokenExample);
        //根据日期和车次进行查询一等座座位个数和二等座座位个数，然后进行令牌信息进行保存
        int ydzCount = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode());
        int edzCount = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode());
        //在设置令牌数的时候进行获取实际座位的70%,从而从根源上避免超卖的情况出现
        double tokenRate = tokenRateProperties.getTokenRate();
        log.info("令牌比例：{}",tokenRate);
        SkToken skToken = new SkToken();
        skToken.setId(SnowUtil.getSnowflakeNextId());
        skToken.setDate(date);
        skToken.setTrainCode(trainCode);
        skToken.setYdzCount((int)(ydzCount * tokenRate));
        skToken.setEdzCount((int)(edzCount * tokenRate));
        skToken.setCreateTime(now);
        skToken.setUpdateTime(now);
        skTokenMapper.insert(skToken);
    }

    //校验令牌信息，返回值0：令牌初始值；1：令牌校验信息失败；2：一等座令牌不足；3：二等座令牌不足；4：令牌校验成功
    public SkTokenEnum validSkToken(Date date, String trainCode, List<ConfirmOrderTicketReq> tickets, Long member) {
        int defaultCount = 5;
        DateTime now = DateTime.now();
        // (2）设置一个资源锁
        RLock resourceLock = null;
        String resourceLockKey = RedisKeyPreEnum.RESOURCE_SK_TOKEN + ":" + DateUtil.formatDate(date) + ":" + trainCode;
        resourceLock = redissonClient.getLock(resourceLockKey);
        try {
            //尝试获取令牌资源锁,使用看门狗
            boolean isResourceLock = resourceLock.tryLock(2, -1, TimeUnit.SECONDS);
            if (!isResourceLock) {
                log.info("很遗憾，没有抢到资源锁！");
                throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
            }
            log.info("恭喜抢到了资源锁！");
            //（2）调用方法获取令牌信息
            SkToken skToken = querySkTokenByDateAndTrainCode(date, trainCode);
            //（3）遍历订单中的所有车票
            for (ConfirmOrderTicketReq ticket : tickets) {
                //获取座位类型
                String seatTypeCode = ticket.getSeatTypeCode();
                //（4）先判断该令牌信息是否在缓存中进行存储，若在缓存中存储则获取缓存数据，若缓存中没有则从数据库中获取
                CacheResultEnum status = reduceSkTokenCache(seatTypeCode, date, trainCode);
                //若缓存中一等座令牌不足，则进行返回在下订单的页面显示一等座票已卖完
                if (status == CacheResultEnum.YDZ_CACHE_NOT_ENOUGH) {
                    return SkTokenEnum.ERROR_YDZ_NOT_ENOUGH;
                }
                //若缓存中二等座令牌不足，则进行返回在下订单的页面显示二等座票已卖完
                else if (status == CacheResultEnum.EDZ_CACHE_NOT_ENOUGH) {
                    return SkTokenEnum.ERROR_EDZ_NOT_ENOUGH;
                }
                //若缓存中既没有一等座也没有二等座，则缓存中令牌信息不存在
                else if (status == CacheResultEnum.ERROR_NOT_FOUND) {
                    return SkTokenEnum.ERROR_NOT_FOUND;
                }
                //若缓存中令牌信息足够，则进行下一个
                else if (status == CacheResultEnum.CACHE_OK) {
                    continue;
                }
                //若缓存中没有存储，则从数据库中获取
                else if (status == CacheResultEnum.NEED_DB_CHECK_MORE || status == CacheResultEnum.NEED_DB_CHECK_ONE || status == CacheResultEnum.NEED_DB_CHECK_LAST) {
                    int reduceCount;
                    if (status == CacheResultEnum.NEED_DB_CHECK_ONE) {
                        reduceCount = 1;
                    } else if (status == CacheResultEnum.NEED_DB_CHECK_MORE) {
                        reduceCount = defaultCount;
                    } else if (status == CacheResultEnum.NEED_DB_CHECK_LAST) {
                        String Prefix = "1".equals(seatTypeCode) ? RedisKeyPreEnum.SK_TOKEN_YDZ_QUERY_COUNTER.getCode() : RedisKeyPreEnum.SK_TOKEN_EDZ_QUERY_COUNTER.getCode();
                        String counterKey = Prefix + ":" + DateUtil.formatDate(date) + ":" + trainCode;
                        Object queryCount = longRedisTemplate.opsForValue().get(counterKey);
                        if (queryCount != null) {
                            reduceCount = Integer.parseInt(queryCount.toString());
                            log.info("最后一次的查询数为：{}", reduceCount);
                        } else {
                            return SkTokenEnum.ERROR_NOT_FOUND;
                        }
                    } else {
                        return SkTokenEnum.ERROR_NOT_FOUND;
                    }
                    int updateCount = skTokenMapperCust.decreaseSeat(skToken.getId(), seatTypeCode, now, reduceCount);
                    //若影响行数为0，则说明令牌不足
                    if (updateCount == 0) {
                        if ("1".equals(seatTypeCode)) {
                            log.info("数据库中一等座令牌不足!");
                            return SkTokenEnum.ERROR_YDZ_NOT_ENOUGH;
                        } else if ("2".equals(seatTypeCode)) {
                            log.info("数据库中二等座令牌不足!");
                            return SkTokenEnum.ERROR_EDZ_NOT_ENOUGH;
                        } else {
                            log.info("数据库出现错误");
                            return SkTokenEnum.ERROR_NOT_FOUND;
                        }
                    } else {
                        //1.进行判断座位类型生成ydz/edz的缓存key前缀
                        String keyPrefix = "1".equals(seatTypeCode) ? RedisKeyPreEnum.SK_TOKEN_YDZ_COUNT.getCode() : RedisKeyPreEnum.SK_TOKEN_EDZ_COUNT.getCode();
                        String LockKey = keyPrefix + ":" + DateUtil.formatDate(date) + ":" + trainCode;
                        //2.更新缓存
                        SkToken cacheSkToken = querySkTokenByDateAndTrainCode(date, trainCode);
                        Long ydzCount = cacheSkToken.getYdzCount().longValue();
                        Long edzCount = cacheSkToken.getEdzCount().longValue();
                        Long cacheCount = "1".equals(seatTypeCode) ? ydzCount : edzCount;
                        //3.将令牌信息存储到缓存中
                        longRedisTemplate.opsForValue().set(LockKey, cacheCount);
                    }
                }
            }
            return SkTokenEnum.SUCCESS;
        } catch (Exception e) {
            log.error("获取资源锁异常：{}", e);
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
        } finally {
            if (resourceLock != null && resourceLock.isLocked() && resourceLock.isHeldByCurrentThread()) {
                log.info("释放资源锁！");
                resourceLock.unlock();
            }
        }
    }

    //统一的缓存扣减的方法
    public CacheResultEnum reduceSkTokenCache(String seatTypeCode, Date date, String trainCode) {
        String keyPrefix = "1".equals(seatTypeCode) ? RedisKeyPreEnum.SK_TOKEN_YDZ_COUNT.getCode() : RedisKeyPreEnum.SK_TOKEN_EDZ_COUNT.getCode();
        String tokenKey = keyPrefix + ":" + DateUtil.formatDate(date) + ":" + trainCode;

        String Prefix = "1".equals(seatTypeCode) ? RedisKeyPreEnum.SK_TOKEN_YDZ_QUERY_COUNTER.getCode() : RedisKeyPreEnum.SK_TOKEN_EDZ_QUERY_COUNTER.getCode();
        String counterKey = Prefix + ":" + DateUtil.formatDate(date) + ":" + trainCode;

        // 执行Lua脚本
        String result = stringRedisTemplate.execute(
                new DefaultRedisScript<>(LUA_SCRIPT, String.class),
                Arrays.asList(tokenKey, counterKey),
                seatTypeCode  // 传递座位类型作为参数
        );
        //如果返回的是NOT_ENOUGH，则根据座位类型返回对应的令牌不足
        if ("NOT_ENOUGH".equals(result)) {
            if ("1".equals(seatTypeCode)) {
                return CacheResultEnum.YDZ_CACHE_NOT_ENOUGH;
            } else {
                return CacheResultEnum.EDZ_CACHE_NOT_ENOUGH;
            }
        }
        //如果返回的是NEED_DB_CHECK_ONE，则返回查询数据库一次
        if ("NEED_DB_CHECK_ONE".equals(result)) {
            return CacheResultEnum.NEED_DB_CHECK_ONE;
        }
        //如果返回的是扣减后的令牌数据：查询次数，则进行判断是否是最后一次
        String[] parts = result.split(":");
        long tokenCount = Long.parseLong(parts[0]);
        long tokenCounter = Long.parseLong(parts[1]);
        //当缓存中数据为空时，进行多次更新数据库
        if (tokenCount == 0) {
            return CacheResultEnum.NEED_DB_CHECK_LAST;
        } else {
            //判断查询次数是否为5，如果为5，则进行更新数据库5次
            if (tokenCounter == 5) {
                longRedisTemplate.opsForValue().set(counterKey, 0L);
                return CacheResultEnum.NEED_DB_CHECK_MORE;
            }
            //如果查询次数不为5，则说明缓存数量充足，进行下一个
            else {
                return CacheResultEnum.CACHE_OK;
            }
        }
    }

    //根据日期和车次查询令牌信息
    public SkToken querySkTokenByDateAndTrainCode(Date date, String trainCode) {
        SkTokenExample skTokenExample = new SkTokenExample();
        SkTokenExample.Criteria criteria = skTokenExample.createCriteria();
        criteria.andDateEqualTo(date);
        criteria.andTrainCodeEqualTo(trainCode);
        List<SkToken> skTokenList = skTokenMapper.selectByExample(skTokenExample);
        if (CollectionUtil.isEmpty(skTokenList)) {
            log.info("数据库中该令牌信息不存在！");
        }
        log.info("在数据库中查询到令牌信息！");
        return  skTokenList.get(0);
    }

    public void setSkTokenCache(String key,Object value){
        //基础的时间设置为10分钟
        long baseExpireTime = TimeUnit.MINUTES.toSeconds(10);
        //设置随机偏移时间,在两分钟内的偏移时间
        long randomOffset = new Random().nextInt(120);
        //获取总的过期时间
        long expireTime = baseExpireTime + randomOffset;
        //设置缓存
        redisTemplate.opsForValue().set(key,value,expireTime,TimeUnit.SECONDS);
    }
}